﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace CodeContrib.Hydrator
{
	public class Hydrator<T>
	{
		private readonly IList<IMap> _defaultTypeMap;
		private readonly IDictionary<string, IMapping> _propertyMap;
		private readonly IList<IMap> _typeMap;

		public Hydrator() : this(DefaultTypeMap.Mappings)
		{
		}

		public Hydrator(IList<IMap> defaultMap)
		{
			_defaultTypeMap = defaultMap;
			_propertyMap = new Dictionary<string, IMapping>();
			_typeMap = new List<IMap>();
		}

		private void AddDefaultTypeMapToTypeMap()
		{
			foreach (var map in _defaultTypeMap)
			{
				_typeMap.Add(map);
			}
		}

		private void AddTypeMapToPropertyMap()
		{
			AddDefaultTypeMapToTypeMap();

			foreach (var propertyInfo in
				typeof (T).GetProperties().Where(x => x.CanWrite && !_propertyMap.ContainsKey(x.Name)))
			{
				var map = _typeMap.Reverse().FirstOrDefault(x => x.Type == propertyInfo.PropertyType && x.IsMatch(propertyInfo));

				if (map != null)
				{
					_propertyMap[propertyInfo.Name] = map.Mapping(propertyInfo);
				}
			}
		}

		public Hydrator<T> For<TType>(Map<TType> map)
		{
			_typeMap.Add(map);

			return this;
		}

		public Hydrator<T> ForAll<TType>(IRandom<TType> random)
		{
			_typeMap.Add(new Map<TType>(random));

			return this;
		}

		public void Hydrate(T instance)
		{
			AddTypeMapToPropertyMap();

			foreach (var mapping in _propertyMap.Values)
			{
				var propertyInfo = instance.GetType().GetProperty(mapping.PropertyInfo.Name, BindingFlags.Public | BindingFlags.Instance);

				if (propertyInfo != null)
				{
					propertyInfo.SetValue(instance, mapping.Next(), null);
				}
			}
		}

		public Hydrator<T> Ignoring<TProperty>(Expression<Func<T, TProperty>> expression)
		{
			var body = expression.Body as MemberExpression;

			if (body == null)
			{
				return this;
			}

			var propertyName = body.Member.Name;

			var propertyInfo = typeof (T).GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

			if (propertyInfo == null)
			{
				return this;
			}

			_propertyMap.Remove(propertyInfo.Name);

			return this;
		}

		public T Next()
		{
			var instance = (T)Activator.CreateInstance(typeof (T));

			Hydrate(instance);

			return instance;
		}

		private void SetPropertyMap<TProperty>(Expression<Func<T, TProperty>> expression, IRandom<TProperty> random)
		{
			var body = expression.Body as MemberExpression;

			if (body == null)
			{
				return;
			}

			var propertyName = body.Member.Name;

			var propertyInfo = typeof (T).GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

			if (propertyInfo == null
			    || !propertyInfo.CanWrite)
			{
				return;
			}

			_propertyMap[propertyInfo.Name] = new Mapping<TProperty>(propertyInfo, random);
		}

		public Hydrator<T> With<TProperty>(Expression<Func<T, TProperty>> expression, IRandom<TProperty> random)
		{
			SetPropertyMap(expression, random);

			return this;
		}
	}
}